﻿#region Copyright and License
/*==============================================================================
 *  Copyright (c) cndotnet.org Corporation.  All rights reserved.
 * ===============================================================================
 * This code and information is provided "as is" without warranty of any kind,
 * either expressed or implied, including but not limited to the implied warranties
 * of merchantability and fitness for a particular purpose.
 * ===============================================================================
 * Licensed under the GNU General Public License (GPL) v2
 * http://www.cndotnet.org/ezsocio
 * ==============================================================================*/
#endregion

#region Using
using System;
using System.Collections;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Data.Linq.Mapping;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using CNdotnet.Ezsocio.Core.Common;
using CNdotnet.Ezsocio.Core.DataEntity;
#endregion

namespace CNdotnet.Ezsocio.Core.Repository
{
    /// <summary>
    /// The base class of repository
    /// </summary>
    public abstract class RepositoryBase
    {
        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="RepositoryBase"/> class.
        /// </summary>
        protected RepositoryBase()
            : this(DefaultConnectionString, true)
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="RepositoryBase"/> class.
        /// </summary>
        /// <param name="fileOrServerOrConnection">The file or server or connection.</param>
        /// <param name="isReadOnly">if set to <c>true</c> [is read only].</param>
        protected RepositoryBase(string fileOrServerOrConnection, bool isReadOnly)
            : this(new RepositoryContext(fileOrServerOrConnection, isReadOnly))
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="RepositoryBase"/> class.
        /// </summary>
        /// <param name="connection">The connection.</param>
        /// <param name="isReadOnly">if set to <c>true</c> [is read only].</param>
        protected RepositoryBase(IDbConnection connection, bool isReadOnly)
            : this(new RepositoryContext(connection, isReadOnly))
        {
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="RepositoryBase"/> class.
        /// </summary>
        /// <param name="context">The context.</param>
        protected RepositoryBase(RepositoryContext context)
        {
            DataContext = context;
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the data context.
        /// </summary>
        /// <value>The data context.</value>
        private RepositoryContext DataContext { get; set; }

        /// <summary>
        /// Gets the context.
        /// </summary>
        /// <value>The context.</value>
        protected IRepositoryContext Context
        {
            get
            {
                return DataContext;
            }
        }

        /// <summary>
        /// Gets or sets a value indicating whether this instance is read only.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance is read only; otherwise, <c>false</c>.
        /// </value>
        public bool IsReadOnly
        {
            get { return Context.IsReadOnly; }
            set { DataContext.ResetContext(value); }
        }

        #endregion

        #region Static Properties

        /// <summary>
        /// Gets the name of the repository connection.
        /// </summary>
        /// <returns></returns>
        protected static string GetRepositoryConnectionName()
        {
            string repositoryConnectionName = ConfigurationManager.AppSettings["RepositoryConnectionName"];
            if (string.IsNullOrEmpty(repositoryConnectionName))
            {
                throw new InvalidOperationException("The applicationsetting key 'RepositoryConnectionName' is not set.");
            }
            return repositoryConnectionName;
        }

        /// <summary>
        /// Gets the default connection string.
        /// </summary>
        /// <value>The default connection string.</value>
        protected static string DefaultConnectionString
        {
            get
            {
                return ConfigurationManager.ConnectionStrings[GetRepositoryConnectionName()].ConnectionString;
            }
        }

        #endregion

        #region Entity and metadata functions

        protected virtual TEntity GetEntity<TEntity>(Expression<Func<TEntity, bool>> queryFunc) where TEntity : class //Entity
        {

            Debug.Assert(queryFunc != null, "expression passed in is null");

            var results = from entity in DataContext.GetTable<TEntity>()
                          select entity;


            results = results.Where(queryFunc);

            return results.FirstOrDefault();
        }

        protected virtual IQueryable<TEntity> GetEntities<TEntity>(Expression<Func<TEntity, bool>> queryFunc) where TEntity : class //Entity 
        {
            Debug.Assert(queryFunc != null, "expression passed in is null");

            var results = from entity in Context.GetTable<TEntity>()
                          select entity;

            results = results.Where(queryFunc);

            return results;
        }

        protected virtual IQueryable<TEntity> GetEntities<TEntity>() where TEntity : class //Entity 
        {
            var results = from entity in Context.GetTable<TEntity>()
                          select entity;

            return results;
        }

        protected MetaTable GetMetaTable(Type rowType)
        {
            return DataContext.Mapping.GetTable(rowType);
        }


        #endregion

        #region Delete and insert, update methods

        #region Delete entity

        protected void DeleteEntity<TEntity>(Expression<Func<TEntity, object>> fieldExpression, object value) where TEntity : class
        {

            MemberExpression memberExpression = null;

            if (fieldExpression != null)
            {
                if (fieldExpression.Body is MemberExpression)
                {
                    memberExpression = (MemberExpression)fieldExpression.Body;
                }
                else if (fieldExpression.Body is UnaryExpression && ((UnaryExpression)fieldExpression.Body).Operand is MemberExpression)
                {
                    memberExpression = ((UnaryExpression)fieldExpression.Body).Operand as MemberExpression;
                }
            }
            if (memberExpression == null)
            {
                throw new NotSupportedException("The given expression is not supported.");
            }

            string fieldName = memberExpression.Member.Name;
            DeleteEntity(typeof(TEntity),
                        new List<KeyValuePair<string, object>>
                            {
                                new KeyValuePair<string, object>(fieldName,value)
                            }
                        );
        }

        protected void DeleteEntity(Type entityType, List<KeyValuePair<string, object>> criterias)
        {
            if (criterias == null || criterias.Count == 0)
            {
                throw new ArgumentException("Some criterias must be set.", "criterias");
            }

            List<object> values = new List<object>();

            MetaTable table = GetMetaTable(entityType);
            string query = GetDeleteStatment(table);
            int criteriaId = 0;

            foreach (var criteria in criterias)
            {
                if (criteriaId > 0)
                {
                    query += " AND ";
                }

                if (criteria.Value is IEnumerable)
                {
                    string ids = string.Empty;
                    query += " " + criteria.Key + " in (";
                    foreach (var item in (IEnumerable)criteria.Value)
                    {
                        if (ids.Length > 0)
                            ids += ",";

                        ids += GetParamTag(criteriaId);
                        values.Add(item);
                        criteriaId++;
                    }
                    if (string.IsNullOrEmpty(ids))
                        throw new ArgumentOutOfRangeException("criterias", criteria, string.Format("The criteria {0} has value as an IEnumerable which contains no elements. This is not supported", criteria.Key));
                    query += ids + ")";

                }
                else
                {
                    query += " " + criteria.Key + " = {" + criteriaId + "}";
                    values.Add(criteria.Value);
                    criteriaId++;
                }

            }

            DataContext.ExecuteCommand(query, values.ToArray());
        }

        protected void DeleteEntity(Entity entity)
        {
            Type entityType = entity.GetType();
            MetaTable table = GetMetaTable(entityType);

            List<object> arguments = new List<object>();
            var primaryKeyColumns = (from dm in table.RowType.PersistentDataMembers
                                     where dm.IsPrimaryKey
                                     select dm).ToList();

            if (primaryKeyColumns.Count == 0)
            {
                throw new NotSupportedException("This method only support entities with primary keys.");
            }

            string query = GetDeleteStatment(table);

            foreach (var primaryField in primaryKeyColumns)
            {
                if (arguments.Count != 0)
                {
                    query += " AND ";
                }

                object val = entityType.GetProperty(primaryField.Name).GetValue(entity, null);
                query += GetEqualStatment(primaryField.MappedName, arguments.Count);
                arguments.Add(val);
            }

            DataContext.ExecuteCommand(query, arguments.ToArray());
        }

        private static string GetDeleteStatment(MetaTable table)
        {

            var isDeletedColumn = (from dm in table.RowType.PersistentDataMembers
                                   where dm.MappedName == "IsDeleted"
                                   select dm).SingleOrDefault();

            string query;
            if (isDeletedColumn == null)
            {
                query = @"DELETE " + table.TableName + " WHERE ";
            }
            else
            {
                query = @"UPDATE " + table.TableName + " SET IsDeleted=1 WHERE ";
            }
            return query;
        }

        private static string GetEqualStatment(string fieldName, int paramId)
        {
            return string.Format("{0} = {1}", fieldName, GetParamTag(paramId));

        }
        private static string GetParamTag(int paramId)
        {
            return "{" + paramId + "}";

        }
        #endregion

        #region Update Entity
        
        protected void UpdateEntity(Entity entity)
        {
            Dictionary<string, object> changes = entity.GetChanges();

            if (!(changes != null && changes.Count > 0))
                return;

            if (entity.IsAlreadySaved())
            {
                throw new InvalidOperationException("An instance of an entity can only be used to save data once.");
            }

            Type entityType = entity.GetType();
            MetaTable table = GetMetaTable(entityType);

            var persistentMembers = table.RowType.PersistentDataMembers;
            var primaryKeyColumns = (from dm in persistentMembers
                                     where dm.IsPrimaryKey
                                     select dm).ToList();

            if (primaryKeyColumns.Count == 0)
                throw new NotSupportedException("This method only supports entities with primary keys.");

            List<object> arguments = new List<object>();
            StringBuilder builder = new StringBuilder();

            foreach (KeyValuePair<string, object> change in changes)
            {
                if ((persistentMembers.Where(m => m.Name == change.Key && !m.IsAssociation)).Count() != 1)
                    continue;

                if (arguments.Count != 0)
                    builder.Append(", ");

                if (change.Value != null)
                {
                    builder.Append(change.Key + " = {" + arguments.Count + "}");
                    arguments.Add(change.Value);
                }
                else
                {
                    // DataContext.ExecuteCommand() does not accept null values in the arguments list. (Differs from the description in MSDN doc)
                    builder.Append(change.Key + " = NULL");
                }
            }

            if (builder.Length == 0)
                return;

            builder.Insert(0, "UPDATE " + table.TableName + " SET ");

            builder.Append(" WHERE ");
            bool firstPrimaryKey = true;

            foreach (var primaryField in primaryKeyColumns)
            {
                if (firstPrimaryKey)
                    firstPrimaryKey = false;
                else
                    builder.Append(" AND ");

                object val = entityType.GetProperty(primaryField.Name).GetValue(entity, null);
                builder.Append(GetEqualStatment(primaryField.MappedName, arguments.Count));
                arguments.Add(val);
            }

            entity.MarkEntitySaved();

            try
            {
                DataContext.ExecuteCommand(builder.ToString(), arguments.ToArray());
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex);
                throw;
            }
        }

        #endregion

        #region Insert Entity

        protected void InsertEntity(Entity entity)
        {
            if (entity == null)
                throw new ArgumentException("The database entity can not be null.");

            Type entityType = entity.GetType();
            MetaTable table = GetMetaTable(entityType);
            MetaDataMember identityDatamember = table.RowType.DBGeneratedIdentityMember;

            List<object> arguments = new List<object>();
            StringBuilder fieldbuilder = new StringBuilder();
            StringBuilder valuebuilder = new StringBuilder();

            fieldbuilder.Append("INSERT INTO " + table.TableName + " (");

            foreach (var member in table.RowType.PersistentDataMembers)
            {
                if (!member.IsAssociation && !member.IsDbGenerated)
                {
                    object value = entityType.GetProperty(member.Name).GetValue(entity, null);
                    if (value != null)
                    {
                        if (arguments.Count != 0)
                        {
                            fieldbuilder.Append(", ");
                            valuebuilder.Append(", ");
                        }

                        fieldbuilder.Append(member.MappedName);
                        valuebuilder.Append("{" + arguments.Count + "}");

                        arguments.Add(value);

                    }
                }
            }

            fieldbuilder.Append(") Values (");
            fieldbuilder.Append(valuebuilder.ToString());
            fieldbuilder.Append(");");

            if (identityDatamember != null)
            {
                fieldbuilder.AppendFormat("SELECT * From {1} Where {0} = (CONVERT(Int,SCOPE_IDENTITY())) ", identityDatamember.MappedName, table.TableName);
                IEnumerable result = DataContext.ExecuteQuery(entityType, fieldbuilder.ToString(), arguments.ToArray());

                foreach (var item in result)
                {
                    PropertyInfo property = entityType.GetProperty(identityDatamember.Name);
                    object pk = property.GetValue(item, null);
                    property.SetValue(entity, pk, null);
                }
            }
            else
            {
                DataContext.ExecuteCommand(fieldbuilder.ToString(), arguments.ToArray());
            }
        }

        #endregion

        #endregion

        #region Stored Procedures

        protected class StoredProcedureArgument
        {
            public string Name { get; set; }

            public object Value { get; set; }

            public StoredProcedureArgument(string name, object value)
            {
                this.Name = name;
                this.Value = value;
            }
        }

        protected IEnumerable<TEntity> ExecuteStoredProcedure<TEntity>(string storedProcedure, params  StoredProcedureArgument[] arguments) where TEntity : Entity
        {
            Debug.Assert(!string.IsNullOrEmpty(storedProcedure), "procedure name was not found");

            if (arguments == null || arguments.Length == 0)
            {
                return this.DataContext.ExecuteQuery<TEntity>(storedProcedure);
            }

            StringBuilder sql = BuildSql(storedProcedure, arguments);

            object[] argList = (from argument in arguments select argument.Value).ToArray();

            return this.DataContext.ExecuteQuery<TEntity>(sql.ToString(), argList);
        }

        protected void ExecuteStoredProcedure(string storedProcedure, params  StoredProcedureArgument[] arguments)
        {
            Debug.Assert(!string.IsNullOrEmpty(storedProcedure), "procedure name was not found");

            if (arguments == null || arguments.Length == 0)
            {
                this.DataContext.ExecuteCommand(storedProcedure);
                return;
            }

            StringBuilder sql = BuildSql(storedProcedure, arguments);

            object[] argList = (from argument in arguments select argument.Value).ToArray();

            this.DataContext.ExecuteCommand(sql.ToString(), argList);

        }

        private static StringBuilder BuildSql(string storedProcedure, IEnumerable<StoredProcedureArgument> arguments)
        {
            StringBuilder sql = new StringBuilder();
            sql.Append("Exec ");
            sql.Append(storedProcedure);
            sql.Append(" ");
            int index = 0;

            foreach (var argument in arguments)
            {
                if (index != 0)
                {
                    sql.Append(", ");
                }
                sql.Append(argument.Name + " = {" + index + "}");

                index++;
            }
            return sql;
        }

        #endregion

        #region Static help methods

        protected static bool IsValueDefined(DateTime? value)
        {
            return (value != null && value != DateTime.MinValue && value != DateTime.MaxValue);
        }

        #endregion




        #region Events

        /// <summary>
        /// Occurs after data saved
        /// </summary>
        public event EventHandler<SavedEventArgs> AfterSaved;

        /// <summary>
        /// Occurs before saved
        /// </summary>
        public event EventHandler<SavedEventArgs> BeforeSaved;

        /// <summary>
        /// Occurs when save failed
        /// </summary>
        public event EventHandler<FailedEventArgs> SaveFailed;

        #endregion

        #region Virtual Methods

        /// <summary>
        /// Called after data saved
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="action">The action.</param>
        protected virtual void OnAfterSaved(Object sender, SaveAction action)
        {
            EventHandler<SavedEventArgs> tempHandler = AfterSaved;
            if (tempHandler != null)
            {
                tempHandler(sender, new SavedEventArgs(sender, action));
            }
        }

        /// <summary>
        /// Called before saved
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="action">The action.</param>
        protected virtual void OnBeforeSaved(Object sender, SaveAction action)
        {
            EventHandler<SavedEventArgs> tempHandler = BeforeSaved;
            if (tempHandler != null)
            {
                tempHandler(sender, new SavedEventArgs(sender, action));
            }
        }

        /// <summary>
        /// Called when save failed
        /// </summary>
        /// <param name="sender">The sender.</param>
        /// <param name="exception">The exception.</param>
        protected virtual void OnSaveFailed(Object sender, Exception exception)
        {
            EventHandler<FailedEventArgs> tempHandler = SaveFailed;
            if (tempHandler != null)
            {
                tempHandler(sender, new FailedEventArgs(sender, exception));
            }
        }

        #endregion
    }
}
